package game;
import java.util.*;

import be.kuleuven.cs.som.annotate.*;

/**
 * A class of worlds involving a width, a height and a variable number of spaceObjects.
 * @invar	| hasProperSpaceObjects()
 * @invar	| isValidHeight(getHeight())
 * @invar	| isValidWidth(getWidth())
 * @author JeroenGillis && ThomasProvoost
 */
public class World {
		
	private final double width;
	private final double height;
	private static final double maxWidth = Double.MAX_VALUE;
	private static final double maxHeight = Double.MAX_VALUE;
	private final List<SpaceObject> objects = new ArrayList<SpaceObject>();
	// of een set gebruiken ipv lijst.
	private final HashMap<SpaceObject, CollisionPredictor> futureCollisions = new HashMap<SpaceObject, CollisionPredictor>();
	// standaard methodes nog implementeren
	
	/**
	 * Create a new default world. The width and height are 1000, the world has no space objects.
	 */
	public World() {
		this(1000,1000);		
	}
	
	/**
	 * Create a new world with given width and height.
	 * @param width
	 * 			The width of this new world.
	 * @param height
	 * 			The height of this new world.
	 * @post	| (new this).getWidth() == width
	 * @post	| (new this).getHeight() == height
	 * @throws	IllegalArgumentException
	 * 			| ! isValidWidth(width) || ! isValidHeight(height)
	 */
	public World(double width, double height) throws IllegalArgumentException {
		if(! isValidWidth(width) || ! isValidHeight(height)) 
			throw new IllegalArgumentException();
		this.width = width;
		this.height = height;
	}
	
	/**
	 * Return the spaceObject associated with this world at the
	 * given index. 
	 * @param  index
	 *         The index of the spaceObject to return.(
	 * @throws IndexOutOfBoundsException         
	 *       | (index < 1) || (index > getNbSpaceObjects())
	 */
	@Basic
	public SpaceObject getSpaceObjectAt(int index) throws IndexOutOfBoundsException {
		return objects.get(index -1);
	}
	
	/** 
	 * Return the number of bullet associated with this ship.
	 */
	@Basic
	public int getNbSpaceObjects() {
		return objects.size();
	}
	
	/**
	 * Return a list of all the spaceObjects associated with this world.
	 */
	public List<SpaceObject> getAllSpaceObjects() {
		return objects;
	}
	
	/**
	 * Add the given spaceObject to the list of objects if the given spaceObjec is a valid spaceObject for this world.
	 * @param	object
	 * 			The given object to be added.
	 * @post	| if(canHaveAsSpaceObject(object)) (new this).containsSpaceObject(object) == true
	 * @throws	IllegalArgumentException
	 * 			| if(!canHaveAsSpaceObject(object)) || containsSpaceObject(object)
	 */
	public void addSpaceObject(SpaceObject object) throws IllegalArgumentException {
		if( ! canHaveAsSpaceObject(object)) 
			throw new IllegalArgumentException();
		else if(containsSpaceObject(object))	
			throw new IllegalArgumentException();
		object.setWorld(this);
		getAllSpaceObjects().add(object);
		makeNewFutureCollisions(object);
	}
	
	/**
	 * Remove the given spaceObject from the list of objects if the list of objects of this world contains the spaceObject.
	 * @param object
	 * 			The object to be removed.
	 * @post	| (new this).containsSpaceObject(object) == false
	 * @throws	IllegalArgumentException
	 * 			| ! containsSpaceObject(object)
	 */
	public void removeSpaceObject(SpaceObject object) throws IllegalArgumentException {
		if(containsSpaceObject(object))
			objects.remove(object);
		else 
			throw new IllegalArgumentException();
		removeFutureCollisions(object);
	}
	
	/**
	 * Return a boolean reflecting whether this world can have the given spaceObject as a object.
	 * @param 	object
	 * 			The spaceObject to be checked.
	 * @return	| if(object == null || object.isTerminated() ) result == false
	 * 			| else result == object.canHaveAsWorld(this)
	 */
	public boolean canHaveAsSpaceObject(SpaceObject object) {
		if(object == null || object.isTerminated() ) return false;
		return object.canHaveAsWorld(this);
	}
	
	/**
	 * Return a boolean reflecting whether this world has valid space objects.
	 * @return	| if ( for each I in 1..getNbSpaceObjecs(): 
	 * 			| 	(canHaveAsSpaceObject(getSpaceObjectAt(I)) &&  getSpaceObjectAt(I).getWorld() == this
	 * 			|	&& object.isInWorldBoundaries()) ) &&
	 * 			| ( for each I,J in 1..getNbSpaceObjects(): getSpaceObjectAt(I).overlap(getSpaceObjectAt(J) == false || I == J ) 	result == true
	 * 			| else result == false
	 */
	public boolean hasProperSpaceObjects() {
		for(SpaceObject object : getAllSpaceObjects()) {
			if(!canHaveAsSpaceObject(object) || object.getWorld() != this || !object.isInWorldBoundaries()) return false;
			for(SpaceObject otherObject : getAllSpaceObjects())
				if(object.overlap(otherObject) && object != otherObject) return false;
		}
		return true;
	}
	
	/**
	 * Return a boolean reflecting whether the list of space objects of this world contains the given object.
	 * @param object
	 * 			The object to be checked.
	 * @return	| result == objects.contains(object)
	 */
	public boolean containsSpaceObject(SpaceObject object) {
		return objects.contains(object);
	}

	/**
	 * Return the final width of this world.
	 */
	public double getWidth() {
		return width;
	}

	/**
	 * Return the final height of this world.
	 */
	public double getHeight() {
		return height;
	}
	
	/**
	 * Return the final max_width of this world.
	 */
	public static double getMaxWidth() {
		return maxWidth;
	}

	/**
	 * Return the final max_height of this world.
	 */
	public static double getMaxHeight() {
		return maxHeight;
	}

	/**
	 * Return a boolean reflecting whether the given height is a valid height for this world.
	 * @param height
	 * 			The height to be checked.
	 * @return	| if ( 0 <= height <= Double.MAX_VALUE ) result == true
	 * 			| else result == false
	 */
	public boolean isValidHeight(double height) {
		return (0 <= height && height <= getMaxHeight());
	}
	
	/**
	 * Return a boolean reflecting whether the given width is a valid width for this world.
	 * @param width
	 * 			The width to be checked.
	 * @return	| if ( 0 <= width <= Double.MAX_VALUE ) result == true
	 * 			| else result == false
	 */
	public boolean isValidWidth(double width) {
		return (0 <= width && width <= getMaxWidth());
	}
	
	/**
	 * Return all the ships in this world.
	 * @return	| for each object in getAllSpaceObjects() 
	 * 			|	if(object instanceof Ship)  getShips().contains(object) == true
	 * 			|	if(!object instanceof Ship)  getShips().contains(object) == false
	 */
	public Set<Ship> getShips() {
		Set<Ship> ships = new HashSet<Ship>();
		for(SpaceObject object : getAllSpaceObjects()) {
			if(object instanceof Ship) 
				ships.add((Ship) object);
		}
		return ships;
	}

	/**
	 * Return all the asteroids in this world.
	 * @return	| for each object in getAllSpaceObjects() 
	 * 			|	if(object instanceof Asteroid)  getAsteroids().contains(object) == true
	 * 			|	if(!object instanceof Asteroid)  getAsteroids().contains(object) == false
	 */
	public Set<Asteroid> getAsteroids() {
		Set<Asteroid> asteroids = new HashSet<Asteroid>();
		for(SpaceObject object : getAllSpaceObjects()) {
			if(object instanceof Asteroid) 
				asteroids.add((Asteroid) object);
		}
		return asteroids;
	}

	/**
	 * Return all the bullets in this world.
	 * @return	| for each object in getAllSpaceObjects() 
	 * 			|	if(object instanceof Bullet)  getBullets().contains(object) == true
	 * 			|	if(!object instanceof Bullet)  getBullets().contains(object) == false
	 */
	public Set<Bullet> getBullets() {
		Set<Bullet> bullets = new HashSet<Bullet>();
		for(SpaceObject object : getAllSpaceObjects()) {
			if(object instanceof Bullet) 
				bullets.add((Bullet) object);
		}
		return bullets;
	}
	

	/**
	 * 
	 * @param dt
	 */
	public void evolve(double dt) {
		SpaceObject[] nextCollision = getNextCollision();
		double timeToNextCollision;
		if(nextCollision[0] == null) {
			timeToNextCollision = Double.POSITIVE_INFINITY;
		}
		else 
			timeToNextCollision = nextCollision[0].getTimeToCollision(nextCollision[1]);

		if(timeToNextCollision > dt) {

			moveSpaceObjects(dt); // N
			thrustShips(); // N
			updateFutureCollisions(getThrustingShips());

		}
		else {

			moveSpaceObjects(timeToNextCollision); // N
			thrustShips(); // N
			resolveCollision(nextCollision); // N or cte
			Collection<SpaceObject> changedObjects = getThrustingShips();
			changedObjects.add(nextCollision[0]); changedObjects.add(nextCollision[1]);
			updateFutureCollisions(changedObjects);
			evolve(dt - timeToNextCollision);
			} 
		}

	/**
	 * Move all space objects in this world.
	 * @param moveTime
	 * 			The time all space objects move.
	 * @effect	| for each object in getAllSpaceObjects() 
	 * 			|	object.move(moveTime)
	 */
	public void moveSpaceObjects(double moveTime) {
		for (SpaceObject object : getAllSpaceObjects()) {
			object.move(moveTime);
			}
	}
	
	/**
	 * Thrusts all ships in this world.
	 * @effect	| for each ship in getShips()
	 *			|		ship.thrust()
	 */
	public void thrustShips() {
		for(Ship ship : getShips()) {
			ship.thrust();
		}
	}
	

	/**
	 * Return a collection of SpaceObjects with their thruster active at the moment this method is invoked.
	 * @return	| for each ship in getShips(): 
	 * 			|	if(ship.isActiveThruster()) getThrustingShips().contains(ship) == true
	 * 			|	else getThrustingShips().contains(ship) == false
	 */
	//public gemaakt om te testen
	public Collection<SpaceObject> getThrustingShips() {
		Collection<SpaceObject> thrustingShips = new ArrayList<SpaceObject>();
		for(Ship ship : getShips()) {
			if(ship.isActiveThruster())
				thrustingShips.add(ship);
		}
		return thrustingShips;
	}
	
	/**
	 * Resolve the collision between the given SpaceObjects.
	 * @param collision
	 * 			The space objects that collide.
	 * @effect	| collision[0].collideWith(collision[1])
	 */
	public void resolveCollision(SpaceObject[] collision) throws IllegalStateException,IllegalArgumentException {
		collision[0].collideWith(collision[1]);
		
	}
	
	/**
	 * Return a array of the two space objects who will be the first of all space objects in this world to collide with each other.
	 * @return	| for each object in getAllSpaceObjects()
	 * 			|	object.getTimeToCollision(for each object2 in getAllSpaceObjects() ) >= 
	 * 			|		getNextCollision()[0].getTimeToCollision(getNextCollision()[1])
	 */
	public SpaceObject[] getNextCollision() {
		Date nearestCollisionMoment = getCollisionPredictorFrom(getSpaceObjectAt(1)).getFirstDate();
		SpaceObject[] collidingSpaceObjects = new SpaceObject[2];
		for(SpaceObject object1 : getAllSpaceObjects()) {
			Date objectNearestCollisionMoment = getCollisionPredictorFrom(object1).getFirstDate();
			if( objectNearestCollisionMoment != null && objectNearestCollisionMoment.before(nearestCollisionMoment)) {
				nearestCollisionMoment = objectNearestCollisionMoment;
				collidingSpaceObjects[0] = getCollisionPredictorFrom(object1).first().getObject1();
				collidingSpaceObjects[1] = getCollisionPredictorFrom(object1).first().getObject2();	
			}
		}
		return collidingSpaceObjects;			
	}
	
	/**
	 * Update all the future collisions of the given collection of spaceObjects. 
	 * @param objects
	 * 			The spaceObjects whose future collisions are updated.
	 * @post	| for each futureCollision in getAllFutureCollisions(objects) 
	 * 			|	futureCollision.getTimeToCollision() == futureCollision.getObject1().getTimeToCollision(futureCollision.getObject2())
	 */
	public void updateFutureCollisions(Collection<SpaceObject> objects) {
		for (SpaceObject object : objects) {
			if(object.isTerminated())
				getAllFutureCollisions().remove(object);
			else
				getCollisionPredictorFrom(object).updateAll();
		}
		for (SpaceObject spaceObject: getAllSpaceObjects()) {
			getCollisionPredictorFrom(spaceObject).update(objects);
		}
	}
	
	/**
	 * 
	 * @return
	 */
	public HashMap<SpaceObject, CollisionPredictor> getAllFutureCollisions() {
		return this.futureCollisions;
	}
	
	/**
	 * Return a list of futureCollisions of the given objects.
	 * @param objects
	 * 			The given spaceObjects.
	 * @return	| for each futureCollision in getAllFutureCollisions(objects) :
	 * 			|	objects.contains(futureCollision.getObject1()) || objects.contains(futureCollision.getObject2())
	 */
	public List<FutureCollision> getAllFutureCollisions(Collection<SpaceObject> objects) {
		ArrayList<FutureCollision> list = new ArrayList<FutureCollision>();
		for(SpaceObject object : objects)
		list.addAll(getAllFutureCollisions().get(object).getOrderedFutureCollisions());
		return list;
	}
	
	/**
	 * Return the collisionPredictor of the given space object.
	 * @param source
	 * 			The space object whose collision predictor is returned.
	 * @return	| getAllFutureCollisions().get(source)
	 */
	public CollisionPredictor getCollisionPredictorFrom(SpaceObject source) {
		return getAllFutureCollisions().get(source);
	}
	
	/**
	 * 
	 * @param object
	 */
	private void makeNewFutureCollisions(SpaceObject object) {
		getAllFutureCollisions().put(object, new CollisionPredictor(object));
		for(SpaceObject spaceObject : getAllSpaceObjects()) {
			if(spaceObject != object) {
				FutureCollision collision = new FutureCollision(object, spaceObject);
				getCollisionPredictorFrom(object).put(spaceObject, collision);
				getCollisionPredictorFrom(spaceObject).put(object, collision);
			}
		}
	}
	
	/**
	 * 
	 * @param object
	 */
	private void removeFutureCollisions(SpaceObject object) {
		getAllFutureCollisions().remove(object);
		for(SpaceObject spaceObject : getAllSpaceObjects()) {
			getCollisionPredictorFrom(spaceObject).remove(object);
		}
	}
	
	
	
	
	
}
